package drds.common.db.xml_mapping.session;

import drds.common.db.xml_mapping.binding.MapperRegistry;
import drds.common.db.xml_mapping.builder.IncompleteElementException;
import drds.common.db.xml_mapping.builder.ResultMapResolver;
import drds.common.db.xml_mapping.builder.xml.XmlStatementBuilder;
import drds.common.db.xml_mapping.executor.BatchExecutor;
import drds.common.db.xml_mapping.executor.Executor;
import drds.common.db.xml_mapping.executor.SimpleExecutor;
import drds.common.db.xml_mapping.executor.loader.ProxyFactory;
import drds.common.db.xml_mapping.executor.loader.cglib.CglibProxyFactory;
import drds.common.db.xml_mapping.executor.loader.javassist.JavassistProxyFactory;
import drds.common.db.xml_mapping.executor.parameter.ParameterHandler;
import drds.common.db.xml_mapping.executor.resultset.DefaultResultSetHandler;
import drds.common.db.xml_mapping.executor.resultset.ResultSetHandler;
import drds.common.db.xml_mapping.executor.statement.RoutingStatementHandler;
import drds.common.db.xml_mapping.executor.statement.StatementHandler;
import drds.common.db.xml_mapping.mapping.*;
import drds.common.db.xml_mapping.parameter_and_result_value_type.JdbcType;
import drds.common.db.xml_mapping.parameter_and_result_value_type.TypeAliasRegistry;
import drds.common.db.xml_mapping.parameter_and_result_value_type.TypeHandlerRegistry;
import drds.common.db.xml_mapping.parsing.XmlNode;
import drds.common.db.xml_mapping.reflection.MetaObject;
import drds.common.db.xml_mapping.reflection.factory.ObjectFactory;
import drds.common.db.xml_mapping.reflection.factory.ObjectFactoryImpl;
import drds.common.db.xml_mapping.reflection.wrapper.DefaultObjectWrapperFactory;
import drds.common.db.xml_mapping.reflection.wrapper.ObjectWrapperFactory;
import drds.common.db.xml_mapping.script.LanguageDriver;
import drds.common.db.xml_mapping.script.LanguageDriverRegistry;
import drds.common.db.xml_mapping.script.defaults.RawLanguageDriver;
import drds.common.db.xml_mapping.script.xml_tags.XMLLanguageDriver;
import drds.common.db.xml_mapping.transaction.JdbcTransactionFactory;
import drds.common.db.xml_mapping.transaction.Transaction;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.function.BiFunction;

@Slf4j
public class Configuration {

  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry();
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
  protected final Map<String, Mapper> keyToMappedStatementMap = new StrictMap<Mapper>("Mapped Statements collection")
    .conflictMessageProducer((savedValue, targetValue) ->
      ". please check " + savedValue.getResource() + " and " + targetValue.getResource());

  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");

  protected final Set<String> loadedResources = new HashSet<>();
  protected final Map<String, XmlNode> sqlRefIdToXmlNodeMap = new StrictMap<>("XML fragments parsed from previous mappers");
  protected final Collection<XmlStatementBuilder> incompleteStatements = new LinkedList<>();

  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();


  protected Environment environment;

  protected boolean mapUnderscoreToCamelCase;
  protected boolean aggressiveLazyLoading;

  protected boolean useGeneratedKeys;
  protected boolean useColumnLabel = true;

  protected boolean callSettersOnNulls;
  protected boolean useActualParamName = true;
  protected boolean returnInstanceForEmptyRow;


  protected JdbcType jdbcTypeForNull = JdbcType.OTHER;
  protected Set<String> lazyLoadTriggerMethods = new HashSet<>(Arrays.asList("equals", "clone", "hashCode", "toString"));


  protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
  protected AutoMappingBehavior autoMappingBehavior = AutoMappingBehavior.PARTIAL;
  protected AutoMappingUnknownColumnBehavior autoMappingUnknownColumnBehavior = AutoMappingUnknownColumnBehavior.NONE;
  protected Properties variables = new Properties();

  protected ObjectFactory objectFactory = new ObjectFactoryImpl();
  protected ObjectWrapperFactory objectWrapperFactory = new DefaultObjectWrapperFactory();

  protected ProxyFactory proxyFactory = new JavassistProxyFactory(); // #224 Using internal Javassist instead of OGNL


  protected Class<?> configurationFactory;

  public Configuration(Environment environment) {
    this();
    this.environment = environment;
  }

  public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);


    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);


    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
  }


  public boolean isCallSettersOnNulls() {
    return callSettersOnNulls;
  }


  public boolean isUseActualParamName() {
    return useActualParamName;
  }

  public boolean isReturnInstanceForEmptyRow() {
    return returnInstanceForEmptyRow;
  }


  public Class<?> getConfigurationFactory() {
    return configurationFactory;
  }


  public boolean isMapUnderscoreToCamelCase() {
    return mapUnderscoreToCamelCase;
  }


  public void addLoadedResource(String resource) {
    loadedResources.add(resource);
  }

  public boolean isResourceLoaded(String resource) {
    return loadedResources.contains(resource);
  }

  public Environment getEnvironment() {
    return environment;
  }

  public void setEnvironment(Environment environment) {
    this.environment = environment;
  }

  public AutoMappingBehavior getAutoMappingBehavior() {
    return autoMappingBehavior;
  }


  /**
   * @since 3.4.0
   */
  public AutoMappingUnknownColumnBehavior getAutoMappingUnknownColumnBehavior() {
    return autoMappingUnknownColumnBehavior;
  }


  public void setProxyFactory(ProxyFactory proxyFactory) {
    if (proxyFactory == null) {
      proxyFactory = new JavassistProxyFactory();
    }
    this.proxyFactory = proxyFactory;
  }

  public boolean isAggressiveLazyLoading() {
    return aggressiveLazyLoading;
  }


  public Set<String> getLazyLoadTriggerMethods() {
    return lazyLoadTriggerMethods;
  }


  public boolean isUseGeneratedKeys() {
    return useGeneratedKeys;
  }


  public ExecutorType getDefaultExecutorType() {
    return defaultExecutorType;
  }


  public boolean isUseColumnLabel() {
    return useColumnLabel;
  }

  public JdbcType getJdbcTypeForNull() {
    return jdbcTypeForNull;
  }


  public Properties getVariables() {
    return variables;
  }

  public void setVariables(Properties variables) {
    this.variables = variables;
  }

  public TypeHandlerRegistry getTypeHandlerRegistry() {
    return typeHandlerRegistry;
  }


  public TypeAliasRegistry getTypeAliasRegistry() {
    return typeAliasRegistry;
  }


  public ObjectFactory getObjectFactory() {
    return objectFactory;
  }

  public void setObjectFactory(ObjectFactory objectFactory) {
    if (objectFactory == null) {
      objectFactory = new ObjectFactoryImpl();
    }
    this.objectFactory = objectFactory;
  }

  public ObjectWrapperFactory getObjectWrapperFactory() {
    return objectWrapperFactory;
  }


  public LanguageDriver getDefaultScriptingLanguageInstance() {
    return languageRegistry.getDefaultDriver();
  }


  public LanguageDriver getLanguageDriver(Class<? extends LanguageDriver> langClass) {
    if (langClass == null) {
      return languageRegistry.getDefaultDriver();
    }
    languageRegistry.register(langClass);
    return languageRegistry.getDriver(langClass);
  }


  public MetaObject newMetaObject(Object object) {
    return MetaObject.forObject(object, objectFactory, objectWrapperFactory);
  }

  public ParameterHandler newParameterHandler(Mapper MappedStatement, Object parameterObject, BindSql bindSql) {
    ParameterHandler parameterHandler = MappedStatement.getLang().createParameterHandler(MappedStatement, parameterObject, bindSql);

    return parameterHandler;
  }

  public ResultSetHandler newResultSetHandler(Executor executor, Mapper MappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                                              ResultHandler resultHandler, BindSql bindSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, MappedStatement, parameterHandler, resultHandler, bindSql, rowBounds);

    return resultSetHandler;
  }

  public StatementHandler newStatementHandler(Executor executor, Mapper MappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BindSql bindSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, MappedStatement, parameterObject, rowBounds, resultHandler, bindSql);

    return statementHandler;
  }


  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }


    return executor;
  }


  public void addResultMap(ResultMap rm) {
    resultMaps.put(rm.getId(), rm);


  }


  public ResultMap getResultMap(String id) {
    return resultMaps.get(id);
  }

  public boolean hasResultMap(String id) {
    return resultMaps.containsKey(id);
  }

  public void addParameterMap(ParameterMap pm) {
    parameterMaps.put(pm.getId(), pm);
  }


  public ParameterMap getParameterMap(String id) {
    return parameterMaps.get(id);
  }


  public void addMappedStatement(Mapper MappedStatement) {
    keyToMappedStatementMap.put(MappedStatement.getId(), MappedStatement);
  }

  public Collection<String> getMappedStatementNames() {
    buildAllStatements();
    return keyToMappedStatementMap.keySet();
  }

  public Collection<Mapper> getKeyToMappedStatementMap() {
    buildAllStatements();
    return keyToMappedStatementMap.values();
  }

  public Collection<XmlStatementBuilder> getIncompleteStatements() {
    return incompleteStatements;
  }

  public void addIncompleteStatement(XmlStatementBuilder incompleteStatement) {
    incompleteStatements.add(incompleteStatement);
  }


  public Collection<ResultMapResolver> getIncompleteResultMaps() {
    return incompleteResultMaps;
  }

  public void addIncompleteResultMap(ResultMapResolver resultMapResolver) {
    incompleteResultMaps.add(resultMapResolver);
  }


  public Mapper getMappedStatement(String id) {
    return this.getMappedStatement(id, true);
  }

  public Mapper getMappedStatement(String id, boolean validateIncompleteStatements) {
    if (validateIncompleteStatements) {
      buildAllStatements();
    }
    return keyToMappedStatementMap.get(id);
  }

  public Map<String, XmlNode> getSqlRefIdToXmlNodeMap() {
    return sqlRefIdToXmlNodeMap;
  }

  public void addMappers(String packageName, Class<?> superType) {
    mapperRegistry.addMappers(packageName, superType);
  }

  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }

  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

  public <T> T getMapper(Class<T> type, MapperContext mapperContext) {
    return mapperRegistry.getMapper(type, mapperContext);
  }

  public boolean hasMapper(Class<?> type) {
    return mapperRegistry.hasMapper(type);
  }

  public boolean hasStatement(String statementName) {
    return hasStatement(statementName, true);
  }

  public boolean hasStatement(String statementName, boolean validateIncompleteStatements) {
    if (validateIncompleteStatements) {
      buildAllStatements();
    }
    return keyToMappedStatementMap.containsKey(statementName);
  }


  /*
   * Parses all the unprocessed statement nodes in the cache. It is recommended
   * to call this method once all the mappers are added as it provides fail-fast
   * statement validation.
   */
  protected void buildAllStatements() {
    parsePendingResultMaps();

    if (!incompleteStatements.isEmpty()) {
      synchronized (incompleteStatements) {
        incompleteStatements.removeIf(x -> {
          x.parseStatementNode();
          return true;
        });
      }
    }

  }

  private void parsePendingResultMaps() {
    if (incompleteResultMaps.isEmpty()) {
      return;
    }
    synchronized (incompleteResultMaps) {
      boolean resolved;
      IncompleteElementException ex = null;
      do {
        resolved = false;
        Iterator<ResultMapResolver> iterator = incompleteResultMaps.iterator();
        while (iterator.hasNext()) {
          try {
            iterator.next().resolve();
            iterator.remove();
            resolved = true;
          } catch (IncompleteElementException e) {
            ex = e;
          }
        }
      } while (resolved);
      if (!incompleteResultMaps.isEmpty() && ex != null) {
        // At least one result map is unresolvable.
        throw ex;
      }
    }
  }

  /**
   * Extracts namespace from fully qualified statement id.
   *
   * @param statementId
   * @return namespace or null when id does not contain period.
   */
  protected String extractNamespace(String statementId) {
    int lastPeriod = statementId.lastIndexOf('.');
    return lastPeriod > 0 ? statementId.substring(0, lastPeriod) : null;
  }


  protected static class StrictMap<V> extends HashMap<String, V> {

    private static final long serialVersionUID = -4950446264854982944L;
    private final String name;
    private BiFunction<V, V, String> conflictMessageProducer;

    public StrictMap(String name) {
      super();
      this.name = name;
    }


    /**
     * Assign a function for producing a conflict error message when contains value with the same key.
     * <p>
     * function arguments are 1st is saved value and 2nd is target value.
     *
     * @param conflictMessageProducer A function for producing a conflict error message
     * @return a conflict error message
     * @since 3.5.0
     */
    public StrictMap<V> conflictMessageProducer(BiFunction<V, V, String> conflictMessageProducer) {
      this.conflictMessageProducer = conflictMessageProducer;
      return this;
    }

    @Override
    @SuppressWarnings("unchecked")
    public V put(String key, V value) {
      if (containsKey(key)) {
        throw new IllegalArgumentException(name + " already contains value for " + key
          + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else {
          super.put(shortKey, (V) new Ambiguity(shortKey));
        }
      }
      return super.put(key, value);
    }

    @Override
    public V get(Object key) {
      V value = super.get(key);
      if (value == null) {
        throw new IllegalArgumentException(name + " does not contain value for " + key);
      }
      if (value instanceof Ambiguity) {
        throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
          + " (try using the full name including the namespace, or rename one of the entries)");
      }
      return value;
    }

    private String getShortName(String key) {
      final String[] keyParts = key.split("\\.");
      return keyParts[keyParts.length - 1];
    }

    protected static class Ambiguity {
      final private String subject;

      public Ambiguity(String subject) {
        this.subject = subject;
      }

      public String getSubject() {
        return subject;
      }
    }
  }

}
